相信大家對於 Widget 的接觸已經很熟悉了,那 Widget 是誰在管理的?這時候要幫忙請出背後的主角了,也就是 Element
,身為 Flutter 高效的核心支柱,它管理著 Widget 和 RenderObject,包含 Pipeline 的 build、layout、paint 幾個工作階段,所以它非常重要。
本文主要說明 Element 身份、生命週期,以及正確的使用方式,希望可以幫大家建立良好的觀念,有效提升開發品質。那我們就不多話了,直接往下開始吧!
BuildContext
runtimeType
和 Key
不同,當 Widget-canUpdate()
返回 false,這時候就會重建與 Widget 的初始互動。以 StatelessWidget 為例,在創建 Element 的同時,會將自己交給 Element 管理。實際上 Widget 就是一個描述配置。
與 RenderObject 的初始互動。以 ErrorWidget 為例,它就是我們在開發時遇到錯誤的紅色畫面。本身沒有 child 所以使用 LeafRenderObjectWidget,本身的 RenderObject 為 RenderErrorBox,那它什麼在什麼時候被創建呢?在 RenderObjectElement 綁到樹上的時候,會透過 Widget 使用 createRenderObject()
產生 原本設置好的 RenderObject,也就是 RenderErrorBox
Element 的特殊 slot
屬性,主要給 Parent 節點用來辨別 child 在 List 的位置。當使用 Element-updateChild()
時,過程中會進行 Element-inflateWidget()
,參數為子元件跟它的 slot 所屬位置。
而前面我們有提到 Element 實際上是 BuildContext
,可以從 StatelessElement 和 StatefulElement 來確認 。
Element 本身實作了 BuildContext
。
當 Element 第一次添加到樹時,進行狀態的初始化。狀態從預設的 initial
轉為 active
。
以 StatefulWidget 的 ComponentElement 為例,一開始都會先呼叫父類的基礎操作,在進行個別處理。會先確保 _lifecycleState
為 active
才進行初始 build。
StatefulElement 覆寫了_firstBuild()
,過程中會陸續檢查和變更 State 的生命週期狀態,接著執行 didChangeDependencies()
,然後透過父類的 _firstBuild()
觸發 ComponentElement-performRebuild()
和 Element-performRebuild()
,最終執行 State 裡的 build()
,將 Widget Tree 創建出來。
激活之前經過 deactivate()
釋放的元素,它們的特點是擁有 GlobalKey,狀態從 inactive
轉為 active
。重新分配舊有的 State 和 Element 給新的 StatefulWidget。
從 StatefulElement 來看,一開始執行 Element-activate()
,當中會檢查狀態是否為 inactive
,將它更新成 active
狀態。最後安排此 Element 和 Widget 進行 rebuild。
在開發時,我們也可以在 State 裡覆寫 activate()
,但通常不太需要用到。
透過新的元件更新 Element 配置,這也是最理想的操作,盡可能地讓 Element 使用 update()
,實現最小消耗。
從 StatefulElement 來看,當使用新的元件更新時,會先執行父類別 Element-update()
,檢查實體不同並且在透過 Widget 的 canUpdate()
檢查 runtimeType 和 key,確保都相同後才會更新配置。
接著更新 State 的 _widget
屬性,並執行 didUpdateWidget(oldWidget)
參數為舊的元件配置,我們在開發時就能透過此方法進行一些狀態操作或是資源釋放。最後執行 rebuild()
當 Element 從 Element Tree 中移除或移動時觸發,狀態從 active
轉為 inactive
。可能 Widget 的 runtimeType 或 key 發生了變化,當其中一個條件不符合時,Element 會從樹上被移除,最終被銷毀是放掉。
不過 Element 還有機會被再次使用,可以經由 Tree Surgery 實現。當元件本身設置 GlobalKey 時,如果元件替換階層或是移動到其他 Widget Tree,Element 和 State 可以再次被激活。在當前幀觸發 activate()
和 update()
,最終回歸 active
狀態。如果沒有被 reactivated 就會被銷毀。
從 StatefulElement 來看,首先會觸發 State 的 deactivate()
,跟 activate()
一樣我們可以做一些處理。
接著呼叫父類 Element-deactivate()
,確保為 active 狀態,最終更新成 inactive
。
如果 Element 在當前 frame 期間沒有被重新激活,它將從 Element Tree 解綁並且不能再重複使用,狀態從 inactive
轉為 defunct
。
從 StatefulElement 來看,會先執行父類的 unmount()
,檢查是否已經 deactivate 並且狀態為 inactive
,接著針對有設置 GlobalKey 的 Element 進行釋放,不需要再保存在清單裡,最終將 _lifecycleState
更新成 defunct
。
接著呼叫 State 的 dispose()
,也是我們很熟悉的方法,負責用來釋放資源。最後將 State 實體和 element 物件都更新成 null,完整地避免記憶體洩漏。這時候 State 也無法在執行 build 了
此範例有一個 HomePage,按鈕打開 BottomSheet 進行顯示,透過 Scaffold.of(context)
取得 ScaffoldState(藉此可以知道 Scaffold 本身是一個 StatefulWidget),使用 showBottomSheet()
開啟,但是這時候會出現無法存取 Scaffold 的相關訊息。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () {
Scaffold.of(context).showBottomSheet(
(context) => const SizedBox(
height: 200,
child: Text('Dash!'),
),
);
},
child: const Text('Hi!'),
),
);
}
}
錯誤訊息:Scaffold.of() called with a context that does not contain a Scaffold.
======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
Scaffold.of() called with a context that does not contain a Scaffold.
由訊息我們可以得知 context 從樹上找不到 ScaffoldState,代表沒有 Scaffold 元件,那是問什麼呢,範例上不是有嗎?我們進到 static of()
方法來一探究竟,其中透過 context 使用了 findAncestorStateOfType()
從祖先、父節點查找 ScaffoldState,找到就直接回傳,否則會丟出例外,也就是我們看到的訊息。
從範例來看,我們使用 Scaffold.of(context)
的 context 是來自於上層,而不是在 Scaffold 裡面的節點,所以往上找當然就找不到。如果 HomePage 第一頁,通常上層就會是根元件的 MaterialApp,那層還沒有使用任何的 Scaffold 元件,所以才會導致錯誤。
在元件外層包裹 Builder
元件,對於該層次提供一個 context
,創建一個樹上節點,讓我們可以存取到之前的祖先、父節點。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (context) {
return TextButton(
onPressed: () {
Scaffold.of(context).showBottomSheet(
(context) => const SizedBox(
height: 200,
child: Text('Dash!'),
),
);
},
child: const Text('Hi!'),
);
}
),
);
}
自定義新的 Widget,可能是 StatelessWidget 或 StatefulWidget,透過 build(context)
提供的 context 來使用。
@override
Widget build(BuildContext context) {
return const Scaffold(
body: HomePageButton(),
);
}
提醒:在開發 UI 時,需確保
context
的位置和來源,準確使用而不要越層級存取
by Daria Orlova
個人建議 Flutter 開發者都能了解 BuildContext
和 Element
,盡可能地熟悉它,知道相關運作原理以及生命週期。它們能在狀態管理的主題上給予非常大的幫助,當發生錯誤時,我們能在第一時間有個概念與想法,才不會發呆不知道如何是好,可以更快且有效地解決問題。
Element
是 Flutter 高效的原因,請盡可能與它保持友善互動。相信很多人跟我一樣,都是看了很多文章、學習每位開發者的理解,再搭配 source code 閱讀,才能越來越熟悉它。總之,投資在這上面是很值得的一件事,當我們能正確使用它的時候,實現高效且高品質的產品就不會很困難了。
Day 3: 最熟悉的朋友 Flutter Widget,你會用但真的了解它嗎?